<?php

require_once 'interface.php';
require_once 'mysqldb.php';
require_once 'mysqlcompiler.php';
require_once 'objects/fobject.php';

class DBStoreResult implements StoreResult
{

	public function __construct ($store, $db_result)
	{
		$this->rs = $db_result;
		$this->store = $store;
		$this->basetype = $this->store->basetype;
		$this->debug = $store->db->debug;
	} // end of __construct
	public function next ()
	{
		$object = NULL;
		if (!isset($this->rs)) {
			debug('[DBStoreResult] resultset is empty');
			return false;
		}
		if ($row = $this->rs->next()) {
			if (isset($row['oid']) && $this->store->hasObject($row['oid'])) {
				$object = $this->store->getObject($row['oid']);
			} else {
				$m = $this->basetype;
				$object = call_user_func_array(array($m, 'load'), array($this->store, $row));
				$this->store->putObject($object);
			}
		}
		return $object;
	} // end of function next

}

class MysqlStore implements ObjectStore
{

	private $config;
	public $db;
	public $code;
	public $qlc;
	public $td;

	public function __construct ($config, RL_DBAL $db, TypeDomain $td)
	{
		$this->config   = $config;
		$this->db       = $db;
		$this->td 		= $td;
		$this->code     = isset($config["code"]) ? $config['code'] : $this->db->code;
		$this->basetype = isset($config['basetype']) ? $config['basetype'] : 'fobject';
		$this->qlc      = $this->buildCompiler();
		// files
		$this->file_store_path = $config['file_store_path'];

		if (empty($this->file_store_path)) {
			$this->errorConfig('file_store_path', 'Path is empty');
		} else
		if (!is_dir($this->file_store_path)) {
			$this->errorConfig('file_store_path', 'directory "'.$this->file_store_path.'" not exists');
		}
	} // end of __construct
	public function iface ($name)
	{
		$iface = NULL;
		if ($this instanceof $name) return $this;
		switch ($name) {
		default:
			$type = 'mysqlstore_'.$name;
			include_once $this->code .'/stores/'.$type.'.php';
			$iface = new $type($this);
		}
		return $iface;
	} // end of function interface

	public function buildCompiler ()
	{
		$compiler = new MysqlQLCompiler();
		$compiler->store = $this;
		return $compiler;
	} // end of function buildCompiler
	public function parseStructQuery ($criteria, $limit=NULL, $offset=NULL)
	{
		$what = array('id'=>'class', 'name'=>'objects');

		$where = NULL;
		while (list($class_name, $properties)=each($criteria)) {
			while (list($property_name, $compare)=each($properties)) {
				$right[   'id'] = 'property';
				$right['class'] = $class_name;
				$right[ 'name'] = $property_name;
				while (list($function, $value)=each($compare)) {
					$cmp[      'id'] = 'cmp';
					$cmp['operator'] = $function;
					$cmp[   'right'] = $right;
					$cmp[    'left'] = array('id'=>'string', 'value'=>$value);
					if ($where) {
						$node[   'id'] = 'and';
						$node['right'] = $where;
						$node[ 'left'] = $cmp;
						$where = $node;
					} else {
						$where = $cmp;
					}
				}
			}
		}

		$limits = NULL;
		if (isset($limit) || isset($offset)) {
			$limits[    'id'] = 'limits';
			$limits[ 'count'] = $limit;
			$limits['offset'] = $offset;
		}

		$select           = array('id' => 'select');
		$select[  'what'] = array('id' => 'selectlist', 'right' => $what);
		$select[ 'where'] = $where;
		$select['limits'] = $limits;

		$query = array('id'=>'query', 'query'=>$select);

		return $query;
	} // end of function compileSimpleQuery

	//* Objects
	public $table = 'objects';
	public $fields = array( 'oid', 'type', 'data', 'ctime', 'mtime', 'is_deleted');
	public function create ($type, $data, $oid=NULL)
	{
		debug(__METHOD__."($type, [data], $oid)","all");
		$td = $this->td;
		$t = $td->getType($type);
		if (!$t) $t = $td->new_type(array('name'=>$type, 'type' => $this->basetype));

		$created = isset($data['ctime']) ? $data['ctime'] : date('Y-m-d H:i:s');
		$lastchanged = isset($data['mtime']) ? $data['mtime'] : NULL;
		unset($data['oid']); // если нужен, oid должен приходить отдельным аргументом

		$object = $td->new_inst($t);
		$object->init($this, $data);
		$object->id=$oid;
		$object->type=$type;
		$object->ctime = $created;
		$object->mtime = $lastchanged;
		$object->is_deleted = 0;
		$object->store = $this;
		if ($object->id) $this->putObject($object);
		return $object;
	} // end of function create
	public function find ($criteria, $limit=NULL, $offset=NULL)
	{
		$this->error = NULL;
		$result = NULL;
		$compiler = $this->qlc;

		if (is_string($criteria)) {
			$params = $limit; // it's params
			debug(__METHOD__."(\"$criteria\", [params])","all");
			$tree = $compiler->parse($criteria);
		} else {
			debug(__METHOD__."([criteria], $limit, $offset)","all");
			$tree = $this->parseStructQuery($criteria, $limit, $offset);
			$params = array();
		}
		if ($tree) {
			$ctx = new stdClass();
			$ctx->params = $params;
			$sql = $compiler->compile($tree, $ctx);
			$result = $this->findSQL($sql);
		} else {
			$this->error = 'QL parse error: '.$compiler->error;
		}

		if ($this->error) error($this->error);

		return $result;
	} // end of function find
	public function findSQL ($sql)
	{
		$result = NULL;
		if (empty($sql)) {
			$this->error = 'SQL query is empty';
		} elseif ($rs = $this->query($sql)) {
			$result = new DBStoreResult($this, $rs);
			$result->method = 'find';
			$result->sql    = $sql;
		}
		return $result;
	} // end of method findSQL

	function query($sql)
	{
		$res = NULL;
		$rs = $this->db->lowlevel->fetch($sql);
		$this->error = $this->db->error;
		if (empty($this->error))
			$res = new MysqlDBResult($this->db, $rs);
		return $res;
	}

	//* Objects
	static $cache = array();
	public function putObject ($o)
	{
		if (isset($o->id) && $o->id) {
			self::$cache[$o->id] = $o;
		}
	}
	public function getObject ($oid)
	{
		if (!$this->hasObject($oid)) {
			$object = $this->find('select objects where oid = :0', array($oid));
			if ( !empty($object) ) {
				self::$cache[$oid] = $object->next();
			}
		}
		return self::$cache[$oid];
	} // end of method getObject
	public function hasObject ($oid)
	{
		return array_key_exists($oid, self::$cache);
	} // end of method hasObject
	public function delete ($oid)
	{
		// для начала так
		$sql = "DELETE FROM ".$this->db->quoteTable($this->table)." WHERE oid=".$oid;
		$status = $this->db->lowlevel->execute($sql);
		unset(self::$cache[$oid]);
		return $status;
	} // end of function delete


	public function save ($row)
	{
		debug(__METHOD__."([row])","all");
		// insert
		$fs = array();
		$vs = array();
		$row['mtime'] = date('Y-m-d H:i:s');
		foreach ($this->fields as $name) {
			if (array_key_exists($name, $row)) {
				$fs [] = $this->db->quoteField($name);
				$vs [] = is_null($row[$name]) ? ' NULL ' : $this->db->quote($row[$name]);
			}
		}

		$sql = 'REPLACE INTO '.$this->db->quoteTable($this->table).'('.implode(', ', $fs).")\n"
			.' VALUES('.implode(', ', $vs).')';
		$res['method'] = 'save';
		$res['sql'] = $sql;
		$res['status'] = $this->db->lowlevel->execute($sql);
		$res['oid'] = $this->db->lowlevel->insertId();

		return $res;
	} // end of function save

	//* Classes
	public function addProperty ($oid, $class_name, $data)
	{
		$fs = array();
		$vs = array();
		$db = $this->db;
		$fs[] = $db->quoteField('oid');
		$vs[] = $db->quote($oid);
		foreach ($data as $name=>$value) {
			$fs [] = $db->quoteField($name);
			$vs [] = is_null($value) ? ' NULL ' : $db->quote($value);
		}

		$eclass_table = $this->quoteClass($class_name);
		$sql = 'INSERT INTO '.$eclass_table.'('.implode(', ', $fs).")\n"
			.' VALUES('.implode(', ', $vs).')';
		$res['method'] = __METHOD__;
		$res['sql'] = $sql;
		$res['status'] = $db->lowlevel->execute($sql);
		return $res;
	} // end of function addClass
	public function delProperty ($oid, $class_name, $data=NULL)
	{
		$db = $this->db;
		$eclass_table = $this->quoteClass($class_name);
		$where = array();
		$where[] = 'oid='.$db->quote($oid);
		if ($data) foreach ($data as $name=>$value) {
			$f = $db->quoteField($name);
			$where[] = ($data[$name] === NULL) ? "$f IS NULL" : "$f=". $db->quote($data[$name]);
		}

		$sql = 'DELETE FROM '.$eclass_table."\n" .' WHERE '.implode(' AND ', $where);
		$res['method'] = __METHOD__;
		$res['sql']    = $sql;
		$res['status'] = $db->lowlevel->execute($sql);
		return $res;
	} // end of function delClass
	public function findProperty ($oid, $class_name, $criteria=array())
	{
		$result = False;
		debug(__METHOD__."($oid, $class_name, [criteria])","all");
		$db = $this->db;

		$criteria[$class_name]['oid']['=='] = $oid;
		$query = $this->parseStructQuery($criteria);
		if ($query) {
			$query['query']['what']['right'] = array('id'=>'class', 'name'=>$class_name);
			$compiler = $this->qlc;
			$sql = $compiler->compile($query);
			if ($sql) {
				$result = $this->query($sql);
				if ($result) {
					$result->method = 'find';
					$result->sql    = $sql;
				}
			}
		} else {
			$this->error = $compiler->error;
		}

		return $result;
	} // end of function find
	public function quoteClass ($name)
	{
		$db = $this->db;
		return $db->quoteTable('class_'.$name);
	} // end of function quoteClass

	//* Files
	//!a manipulator
	public function addFile ($source=NULL)
	/**
	 * Добавить файл в store.
	 *
	 * Вернет уникальное имя файла в store.
	 *
	 * Если $source содержит имя файла, то скопирует его содержимое.
	 *
	 * @param mixed $source файл-источник
	 * @access public
	 * @return $path имя файла, сохраненного в БД
	 */
	{
		$dest_name = tempnam($this->getFileStorePath(), 'store');

		if ($source==NULL) {
			return $dest_name;
		}

		if ( move_uploaded_file($source, $dest_name) ) {
			return $dest_name;
		} elseif ( copy($source, $dest_name) ) {
				return $dest_name;
			} else return false;

	} // end of function addFile

	public function getFileStorePath ()
	{
		return $this->file_store_path;
	} // end of function getFileStorePath

	//* Errors
	public function errorConfig ($param, $msg='')
	{
		error('[Store] Configuration param error ('.$param.')'.($msg ? ': ' .$msg:''));
	} // end of function errorConfig
}

/*
$rl_lib_dir = dirname(dirname(__FILE__));
ini_set('include_path', ini_get('include_path').PATH_SEPARATOR.$rl_lib_dir);
require_once $rl_lib_dir.'/includes/debug.php';
require_once $rl_lib_dir.'/RL/DBAL.php';
$config = array(
	'code' => dirname(dirname(__FILE__)),
	'db_al' => 'mysql',
	'db_name' => 'karni',
	'db_host' => 'localhost',
	'db_user' => 'root',
	'db_password' => '123',
	'db_prefix' => 'store_',
);
$db = new RL_DBAL(NULL);
$db->loadConfig($config);
$db->connect();
$store = new mysqlstore(array_merge($config, array('db'=>$db)));

$news = $store->create('fnews', array('title' => 'foo', 'date' => '20071001'));
$res = $news->save();
var_dump($res);
$res = $news->inSense("news");
// in sense: type -> class
// type in the category: type -> type
// typeX of typeY: typeY typeX
// typeX typeY: type
// with property as x: class.property <- x
// name as x: let name = x
// x is y: x <=> y
//$res = $news->inSense(
//	"message in the thread in the forum in the community
//	         with user as author in sense (author of forum message)
//	         where community is $community, forum is $forum, user is $user
//	");
var_dump($res);
$res = $news->save();
var_dump($res);

$c['news']['title']['==']='foo';
$c['objects']['type']['==']='fnews';
$rs = $store->find($c, 1);
var_dump($rs->sql);

while ($obj = $rs->next()) {
	var_dump($obj->data);
}

//$ql       = 'objects';
//$ql       = 'select objects';
//$ql       = 'objects where objects.type == name or objects.name == 1';
//$ql       = 'where objects.type == "foo"';
//$ql       = '10 objects where news.title == "foo"';
//$ql       = 'objects limit 10, 20';
// find with params
$ql       = '10 objects where news.title =~ :regexp';
$ql = 'objects where allfeed.tdi_oid==10 or allfeed.tdi_oid==11';
$ql = 'objects where allfeed.tdi_oid = 59';
$ql =  'objects where (objects.type == "doc" or objects.type == "doc") and allfeed.tdi_oid = 59 order by ctime desc limit 0,5';
$ql =  'objects where (objects.type == "doc" and objects.type == "doc") or allfeed.tdi_oid = 59 order by ctime desc limit 0,5';
$rs = $store->find($ql);

if ($rs) {
	var_dump($rs->sql);
	while ($obj = $rs->next()) var_dump($obj->id,$obj->data);
} else {
	var_dump($store->error);
}
*/
